/*-------------------------------------------------------------------------
 *
 * checkpointer.c
 *
 * 检查点进程（checkpointer）是自 PostgreSQL 9.2 起的新特性。
 * 它处理所有检查点。在自上一个检查点以来经过了一定的时间后，检查点会
 * 自动调度，并且也可以根据需要被信号执行。（强制每隔一定数量的 WAL 段进行
 * 检查点的 GUC 参数是通过让后端在填满 WAL 段时发出信号来实现的；
 * 检查点进程本身不会监视条件）。
 *
 * 正常终止是通过 SIGUSR2，这会指示检查点进程执行一个
 * 关机检查点，然后 exit(0)。（在发出 SIGUSR2 之前，必须停止所有后端！）
 * 紧急终止是通过 SIGQUIT；与任何后端一样，检查点进程在 SIGQUIT 时将简单地中止并退出。
 *
 * 如果检查点进程意外退出，postmaster 将此视为与后端崩溃相同：
 * 共享内存可能已损坏，因此剩余后端应该被 SIGQUIT 杀死，然后启动恢复周期。
 * （即使共享内存未损坏，我们也丢失了关于哪些文件需要在下一个检查点
 * 进行 fsync 的信息，因此需要强制系统重启。）
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 *
 *
 * IDENTIFICATION
 *	  src/backend/postmaster/checkpointer.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <sys/time.h>
#include <time.h>

#include "access/xlog.h"
#include "access/xlog_internal.h"
#include "access/xlogrecovery.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/bgwriter.h"
#include "postmaster/interrupt.h"
#include "replication/syncrep.h"
#include "storage/bufmgr.h"
#include "storage/condition_variable.h"
#include "storage/fd.h"
#include "storage/ipc.h"
#include "storage/lwlock.h"
#include "storage/proc.h"
#include "storage/procsignal.h"
#include "storage/shmem.h"
#include "storage/smgr.h"
#include "storage/spin.h"
#include "utils/guc.h"
#include "utils/memutils.h"
#include "utils/resowner.h"


/*----------
 * 用于表示检查点和后端之间通信的共享内存区域
 *
 * ckpt 计数器允许后端监视它们发送的检查点请求的完成情况。
 * 其工作原理如下：
 *	* 在检查点开始时，检查点进程读取（并清除）请求标志并递增 ckpt_started，同时保持 ckpt_lck。
 *	* 在检查点完成时，检查点进程将 ckpt_done 设置为等于 ckpt_started。
 *	* 在检查点失败时，检查点进程递增 ckpt_failed 并将 ckpt_done 设置为等于 ckpt_started。
 *
 * 后端的算法是：
 *	1. 记录 ckpt_failed 和 ckpt_started 的当前值，并设置请求标志，同时保持 ckpt_lck。
 *	2. 发送信号以请求检查点。
 *	3. 睡眠直到 ckpt_started 发生变化。现在你知道自你启动此算法以来，检查点已经开始（虽然*并不是*说
 *	  它是由你的信号特别发起的），并且它正在使用你的标志。
 *	4. 记录 ckpt_started 的新值。
 *	5. 睡眠直到 ckpt_done >= 保存的 ckpt_started 值。（在这里使用模运算以防计数器回绕。）现在你知道检查点
 *	  已经开始并完成，但不知道是否成功。
 *	6. 如果 ckpt_failed 与最初保存的值不同，假设请求失败；否则绝对成功。
 *
 * ckpt_flags 保存了自上次检查点开始以来所有请求后端发送的检查点请求标志的或操作。
 * 这些标志的选择确保或操作是组合多个请求的正确方法。
 *
 * num_backend_writes 用于计算用户后端进程执行的缓冲区写入次数。该计数器应该足够宽，以便
 * 在单次处理周期中不会溢出。num_backend_fsync 用于计算那些也必须做自己的 fsync 的写入子集，
 * 因为检查点进程未能吸收它们的请求。
 *
 * requests 数组保存了由后端发送且尚未被检查点进程吸收的 fsync 请求。
 *
 * 与检查点字段不同，num_backend_writes、num_backend_fsync 和 requests 字段受 CheckpointerCommLock 保护。
 *----------
 */
typedef struct
{
	SyncRequestType type;		/* 请求类型 */
	FileTag		ftag;			/* 文件标识符 */
} CheckpointerRequest;

typedef struct
{
	pid_t		checkpointer_pid;	/* PID（如果未启动则为 0） */

	slock_t		ckpt_lck;		/* 保护所有 ckpt_* 字段 */

	int			ckpt_started;	/* 检查点开始时的推进 */
	int			ckpt_done;		/* 检查点完成时的推进 */
	int			ckpt_failed;	/* 检查点失败时的推进 */

	int			ckpt_flags;		/* 检查点标志，如 xlog.h 中定义的 */

	ConditionVariable start_cv; /* 当 ckpt_started 推进时发出信号 */
	ConditionVariable done_cv;	/* 当 ckpt_done 推进时发出信号 */

	uint32		num_backend_writes; /* 计数用户后端缓冲区写入 */
	uint32		num_backend_fsync;	/* 计数用户后端 fsync 调用 */

	int			num_requests;	/* 当前请求的数量 */
	int			max_requests;	/* 分配的数组大小 */
	CheckpointerRequest requests[FLEXIBLE_ARRAY_MEMBER];
} CheckpointerShmemStruct;

static CheckpointerShmemStruct *CheckpointerShmem;

/* 在 CheckpointWriteDelay 中调用 AbsorbSyncRequests 的时间间隔 */
#define WRITES_PER_ABSORB		1000

/*
 * GUC参数
 */
int			CheckPointTimeout = 300;
int			CheckPointWarning = 30;
double		CheckPointCompletionTarget = 0.9;

/*
 * 私有状态
 */
static bool ckpt_active = false;

/* 当 ckpt_active 为 true 时，这些值有效： */
static pg_time_t ckpt_start_time;
static XLogRecPtr ckpt_start_recptr;
static double ckpt_cached_elapsed;

static pg_time_t last_checkpoint_time;
static pg_time_t last_xlog_switch_time;

/* 私有函数的原型 */

static void fc_HandleCheckpointerInterrupts(void);
static void fc_CheckArchiveTimeout(void);
static bool fc_IsCheckpointOnSchedule(double fc_progress);
static bool fc_ImmediateCheckpointRequested(void);
static bool fc_CompactCheckpointerRequestQueue(void);
static void fc_UpdateSharedMemoryConfig(void);

/* 信号处理程序 */
static void fc_ReqCheckpointHandler(SIGNAL_ARGS);


/*
 * 检查点进程的主要入口点
 *
 * 这是从 AuxiliaryProcessMain 调用的，该进程已经创建了
 * 基本执行环境，但尚未启用信号。
 */
void CheckpointerMain(void)
{
	sigjmp_buf	fc_local_sigjmp_buf;
	MemoryContext fc_checkpointer_context;

	CheckpointerShmem->checkpointer_pid = MyProcPid;

	/*
	 * 正确接受或忽略可能由主进程发送的信号
	 *
	 * 注意：我们故意忽略 SIGTERM，因为在标准 Unix
	 * 系统关闭周期中，init 会同时向所有进程发送 SIGTERM。
	 * 我们想等待后端退出，届时主进程会告诉我们可以关闭了（通过 SIGUSR2）。
	 */
	pqsignal(SIGHUP, SignalHandlerForConfigReload);
	pqsignal(SIGINT, fc_ReqCheckpointHandler); /* 请求检查点 */
	pqsignal(SIGTERM, SIG_IGN); /* 忽略 SIGTERM */
	/* SIGQUIT 处理程序已经由 InitPostmasterChild 设置 */
	pqsignal(SIGALRM, SIG_IGN);
	pqsignal(SIGPIPE, SIG_IGN);
	pqsignal(SIGUSR1, procsignal_sigusr1_handler);
	pqsignal(SIGUSR2, SignalHandlerForShutdownRequest);

	/*
	 * 重置一些由postmaster接受但这里不接受的信号
	 */
	pqsignal(SIGCHLD, SIG_DFL);

	/*
	 * 初始化，以确保首次时间驱动事件在正确的时间发生。
	 */
	last_checkpoint_time = last_xlog_switch_time = (pg_time_t) time(NULL);

	/*
	 * 在关闭后写出统计信息。正常关闭过程中需要恰好由一个
	 * 进程调用此方法，而且由于检查点进程要在非常晚的时候
	 * 关闭...
	 *
	 * Walsenders 在检查点之后关闭，但目前不
	 * 报告统计信息。如果情况改变，我们需要一个更复杂的解决方案。
	 */
	before_shmem_exit(pgstat_before_server_shutdown, 0);

	/*
	 * 创建一个内存上下文，我们将所有工作都在这里完成。我们这样做是为了
	 * 在错误恢复期间重置上下文，从而避免
	 * 可能的内存泄漏。以前这段代码在
	 * TopMemoryContext中运行，但重置它会是个非常糟糕的主意。
	 */
	fc_checkpointer_context = AllocSetContextCreate(TopMemoryContext,
												 "Checkpointer",
												 ALLOCSET_DEFAULT_SIZES);
	MemoryContextSwitchTo(fc_checkpointer_context);

	/*
	 * 如果遇到异常，处理将在这里恢复。
	 *
	 * 你可能会想为什么这不是在一个围绕
	 * PG_TRY构造的无限循环中编码的。原因是这是异常堆栈的底部，
	 * 因此在CATCH部分的过程中完全没有异常处理程序
	 * 生效。通过保持最外层的setjmp
	 * 始终处于活动状态，我们至少有一些机会在错误
	 * 恢复期间恢复错误。（如果由此陷入无限循环，它将很快因overflow
	 * elog.c的内部状态堆栈而停止。）
	 *
	 * 请注意，我们使用sigsetjmp(..., 1)，因此当长跳转到这里时，
	 * 当前信号掩码（即BlockSig）将被恢复。因此，
	 * 除了SIGQUIT之外的信号将被阻止，直到我们完成错误
	 * 恢复。这一政策看似使HOLD_INTERRUPTS()
	 * 调用变得多余，但实际上并非如此，因为InterruptPending可能
	 * 已经被设置。
	 */
	if (sigsetjmp(fc_local_sigjmp_buf, 1) != 0)
	{
		/* 由于未使用 PG_TRY，必须手动重置错误堆栈 */
		error_context_stack = NULL;

		/* 在清理期间防止中断 */
		HOLD_INTERRUPTS();

		/* 将错误报告到服务器日志 */
		EmitErrorReport();

		/*
		 * 这些操作实际上只是 AbortTransaction() 的一个最小子集。
		 * 在检查点进程中我们不需要担心很多资源，但我们确实有 LWLocks、
		 * 缓冲区和临时文件。
		 */
		LWLockReleaseAll();
		ConditionVariableCancelSleep();
		pgstat_report_wait_end();
		AbortBufferIO();
		UnlockBuffers();
		ReleaseAuxProcessResources(false);
		AtEOXact_Buffers(false);
		AtEOXact_SMgr();
		AtEOXact_Files(false);
		AtEOXact_HashTables(false);

		/* 警告任何等待的后端检查点失败。 */
		if (ckpt_active)
		{
			SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
			CheckpointerShmem->ckpt_failed++;
			CheckpointerShmem->ckpt_done = CheckpointerShmem->ckpt_started;
			SpinLockRelease(&CheckpointerShmem->ckpt_lck);

			ConditionVariableBroadcast(&CheckpointerShmem->done_cv);

			ckpt_active = false;
		}

		/*
		 * 现在返回到正常的顶级上下文并清除 ErrorContext，以便下次使用。
		 */
		MemoryContextSwitchTo(fc_checkpointer_context);
		FlushErrorState();

		/* 刷新顶级上下文中任何泄漏的数据 */
		MemoryContextResetAndDeleteChildren(fc_checkpointer_context);

		/* 现在我们可以再次允许中断 */
		RESUME_INTERRUPTS();

		/*
		 * 在任何错误后至少睡眠1秒。写入错误很可能会
		 * 重复发生，我们不想尽可能快地填满错误日志。
		 */
		pg_usleep(1000000L);

		/*
		 * 在任何错误后关闭所有打开的文件。这对Windows很有帮助，
		 * 因为保持已删除的文件打开会导致各种奇怪的错误。
		 * 目前不清楚我们是否需要在其他地方这样做，但应该不会有坏处。
		 */
		smgrcloseall();
	}

	/* 我们现在可以处理 ereport(ERROR) */
	PG_exception_stack = &fc_local_sigjmp_buf;

	/*
	 * 解锁信号（它们在postmaster分叉我们时被阻止）
	 */
	PG_SETMASK(&UnBlockSig);

	/*
	 * 确保所有共享内存值根据配置正确设置。在这里执行
	 * 这个可以确保没有来自其他并发更新者的竞争条件。
	 */
	fc_UpdateSharedMemoryConfig();

	/*
	 * 宣传我们的闩锁，后端可以用它在我们
	 * 睡眠时唤醒我们。
	 */
	ProcGlobal->checkpointerLatch = &MyProc->procLatch;

	/*
	 * 永远循环
	 */
	for (;;)
	{
		bool		fc_do_checkpoint = false;
		int			fc_flags = 0;
		pg_time_t	fc_now;
		int			fc_elapsed_secs;
		int			fc_cur_timeout;

		/* 清除任何已经等待的唤醒 */
		ResetLatch(MyLatch);

		/*
		 * 处理最近收到的任何请求或信号。
		 */
		AbsorbSyncRequests();
		fc_HandleCheckpointerInterrupts();

		/*
		 * 通过检查共享内存中的标志字是否非零来检测待处理的检查点请求。
		 * 我们不应该需要为此获取 ckpt_lck。
		 */
		if (((volatile CheckpointerShmemStruct *) CheckpointerShmem)->ckpt_flags)
		{
			fc_do_checkpoint = true;
			PendingCheckpointerStats.requested_checkpoints++;
		}

		/*
		 * 如果自上次检查点以来经过的时间过长，则强制执行检查点。
		 * 请注意，我们仅在没有外部请求的情况下将定时检查点计入统计
		 * 数据，但如果还有外部请求，我们仍然设置 CAUSE_TIME 标志位。
		 */
		fc_now = (pg_time_t) time(NULL);
		fc_elapsed_secs = fc_now - last_checkpoint_time;
		if (fc_elapsed_secs >= CheckPointTimeout)
		{
			if (!fc_do_checkpoint)
				PendingCheckpointerStats.timed_checkpoints++;
			fc_do_checkpoint = true;
			fc_flags |= CHECKPOINT_CAUSE_TIME;
		}

		/*
		 * 如果请求，则执行检查点。
		 */
		if (fc_do_checkpoint)
		{
			bool		fc_ckpt_performed = false;
			bool		fc_do_restartpoint;

			/* 检查我们是否应该执行检查点或重启点。 */
			fc_do_restartpoint = RecoveryInProgress();

			/*
			 * 原子性地获取请求标志，以确定我们应该执行什么类型的
			 * 检查点，并增加启动计数器以确认我们已开始新的检查点。
			 */
			SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
			fc_flags |= CheckpointerShmem->ckpt_flags;
			CheckpointerShmem->ckpt_flags = 0;
			CheckpointerShmem->ckpt_started++;
			SpinLockRelease(&CheckpointerShmem->ckpt_lck);

			ConditionVariableBroadcast(&CheckpointerShmem->start_cv);

			/*
			 * 恢复结束时的检查点是真正的检查点，它是在我们仍处于恢复中时
			 * 执行的。
			 */
			if (fc_flags & CHECKPOINT_END_OF_RECOVERY)
				fc_do_restartpoint = false;

			/*
			 * 如果 (a) 自上次检查点以来时间过短（无论是什么
			 * 引起的）和 (b) 自上次检查点开始以来有人设置了 CHECKPOINT_CAUSE_XLOG 标志，我们会警告。
			 * 特别注意，这个实现不会产生由 CheckPointTimeout < CheckPointWarning 
			 * 引起的警告。
			 */
			if (!fc_do_restartpoint &&
				(fc_flags & CHECKPOINT_CAUSE_XLOG) &&
				fc_elapsed_secs < CheckPointWarning)
				ereport(LOG,
						(errmsg_plural("checkpoints are occurring too frequently (%d second apart)",
									   "checkpoints are occurring too frequently (%d seconds apart)",
									   fc_elapsed_secs,
									   fc_elapsed_secs),
						 errhint("Consider increasing the configuration parameter \"max_wal_size\".")));

			/*
			 * 初始化在检查点期间使用的检查点私有变量。
			 */
			ckpt_active = true;
			if (fc_do_restartpoint)
				ckpt_start_recptr = GetXLogReplayRecPtr(NULL);
			else
				ckpt_start_recptr = GetInsertRecPtr();
			ckpt_start_time = fc_now;
			ckpt_cached_elapsed = 0;

			/*
			 * 执行检查点。
			 */
			if (!fc_do_restartpoint)
			{
				CreateCheckPoint(fc_flags);
				fc_ckpt_performed = true;
			}
			else
				fc_ckpt_performed = CreateRestartPoint(fc_flags);

			/*
			 * 在任何检查点之后，关闭所有smgr文件。这是为了我们
			 * 不会无限期地保留对已删除文件的smgr引用。
			 */
			smgrcloseall();

			/*
			 * 向任何等待的后端指示检查点完成。
			 */
			SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
			CheckpointerShmem->ckpt_done = CheckpointerShmem->ckpt_started;
			SpinLockRelease(&CheckpointerShmem->ckpt_lck);

			ConditionVariableBroadcast(&CheckpointerShmem->done_cv);

			if (fc_ckpt_performed)
			{
				/*
				 * 请注意，我们记录的是检查点开始时间而不是结束时间
				 * last_checkpoint_time。这是为了使时间驱动的
				 * 检查点发生在可预测的间隔内。
				 */
				last_checkpoint_time = fc_now;
			}
			else
			{
				/*
				 * 我们无法执行重启点（在发生错误时检查点
				 * 抛出一个错误）。最有可能是因为自从上次重启点
				 * 以来，我们没有收到任何新的检查点WAL记录。请在15秒后重试。
				 */
				last_checkpoint_time = fc_now - CheckPointTimeout + 15;
			}

			ckpt_active = false;

			/* 我们可能在检查点期间收到了中断。 */
			fc_HandleCheckpointerInterrupts();
		}

		/* 检查archive_timeout并在必要时切换xlog文件。 */
		fc_CheckArchiveTimeout();

		/* 向累积统计系统报告待处理统计信息 */
		pgstat_report_checkpointer();
		pgstat_report_wal(true);

		/*
		 * 如果设置了任何检查点标志，请重新执行循环以处理
		 * 检查点而不休眠。
		 */
		if (((volatile CheckpointerShmemStruct *) CheckpointerShmem)->ckpt_flags)
			continue;

		/*
		 * 睡眠，直到我们收到信号或是另一个检查点或
		 * xlog文件切换的时间。
		 */
		fc_now = (pg_time_t) time(NULL);
		fc_elapsed_secs = fc_now - last_checkpoint_time;
		if (fc_elapsed_secs >= CheckPointTimeout)
			continue;			/* 我们不需要休眠…… */
		fc_cur_timeout = CheckPointTimeout - fc_elapsed_secs;
		if (XLogArchiveTimeout > 0 && !RecoveryInProgress())
		{
			fc_elapsed_secs = fc_now - last_xlog_switch_time;
			if (fc_elapsed_secs >= XLogArchiveTimeout)
				continue;		/* 我们不需要休眠…… */
			fc_cur_timeout = Min(fc_cur_timeout, XLogArchiveTimeout - fc_elapsed_secs);
		}

		(void) WaitLatch(MyLatch,
						 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
						 fc_cur_timeout * 1000L /* 转换为毫秒 */ ,
						 WAIT_EVENT_CHECKPOINTER_MAIN);
	}
}

/*
 * 处理任何新的中断。
 */
static void fc_HandleCheckpointerInterrupts(void)
{
	if (ProcSignalBarrierPending)
		ProcessProcSignalBarrier();

	if (ConfigReloadPending)
	{
		ConfigReloadPending = false;
		ProcessConfigFile(PGC_SIGHUP);

		/*
		 * 检查点程序是最后一个关闭的进程，因此我们要求它保留
		 * 许多其他任务所需的密钥，其中大多数与检查点完全无关。
		 *
		 * 出于各种原因，一些配置值可以动态更改，因此
		 * 它们的主副本保存在共享内存中，以确保所有
		 * 后端看到相同的值。我们让检查点程序负责
		 * 更新共享内存副本，如果参数设置因SIGHUP而更改。
		 */
		fc_UpdateSharedMemoryConfig();
	}
	if (ShutdownRequestPending)
	{
		/*
		 * 从这里开始，elog(ERROR)应该以exit(1)结束，而不是将控制
		 * 返回到上面的sigsetjmp块
		 */
		ExitOnAnyError = true;

		/*
		 * 关闭数据库。
		 *
		 * 由于ShutdownXLOG()创建重启点或检查点，并
		 * 更新统计信息，增加检查点请求并刷新
		 * 待处理统计信息。
		 */
		PendingCheckpointerStats.requested_checkpoints++;
		ShutdownXLOG(0, 0);
		pgstat_report_checkpointer();
		pgstat_report_wal(true);

		/* 检查点程序正常退出在这里 */
		proc_exit(0);			/* 完成 */
	}

	/* 执行此进程的内存上下文日志记录 */
	if (LogMemoryContextPending)
		ProcessLogMemoryContextInterrupt();
}

/*
 * CheckArchiveTimeout -- 检查archive_timeout并切换xlog文件
 *
 * 如果当前WAL文件中记录了有意义的活动，则将切换到新的WAL文件并强制归档文件写入。这包括大多数写入，包括仅一个检查点记录，但不包括
 * 以XLOG_MARK_UNIMPORTANT标志插入的WAL记录（如正在运行的事务的快照）。
 * 这样的记录根据配置，发生在规则间隔内，并不包含重要的信息。 这样可以避免生成包含少量不重要记录的归档。
 */
static void fc_CheckArchiveTimeout(void)
{
	pg_time_t	fc_now;
	pg_time_t	fc_last_time;
	XLogRecPtr	fc_last_switch_lsn;

	if (XLogArchiveTimeout <= 0 || RecoveryInProgress())
		return;

	fc_now = (pg_time_t) time(NULL);

	/* 首先我们使用可能过时的本地状态进行快速检查。 */
	if ((int) (fc_now - last_xlog_switch_time) < XLogArchiveTimeout)
		return;

	/*
	 * 更新本地状态……请注意，last_xlog_switch_time是执行*或请求*切换的最后时间。
	 */
	fc_last_time = GetLastSegSwitchData(&fc_last_switch_lsn);

	last_xlog_switch_time = Max(last_xlog_switch_time, fc_last_time);

	/* 现在我们可以进行真正的检查 */
	if ((int) (fc_now - last_xlog_switch_time) >= XLogArchiveTimeout)
	{
		/*
		 * 仅当自上次段切换以来记录了“重要”的WAL时切换段（last_switch_lsn指向段切换发生的结尾）。
		 */
		if (GetLastImportantRecPtr() > fc_last_switch_lsn)
		{
			XLogRecPtr	fc_switchpoint;

			/* 将切换标记为不重要，避免触发检查点 */
			fc_switchpoint = RequestXLogSwitch(true);

			/*
			 * 如果返回的指针精确地指向段边界，
			 * 假设什么也没发生。
			 */
			if (XLogSegmentOffset(fc_switchpoint, wal_segment_size) != 0)
				elog(DEBUG1, "write-ahead log switch forced (archive_timeout=%d)",
					 XLogArchiveTimeout);
		}

		/*
		 * 无论如何更新状态，这样当系统空闲时我们就不会不断重试。
		 */
		last_xlog_switch_time = fc_now;
	}
}

/*
 * 如果存在即时检查点请求，则返回true。（请注意，
 * 这并不检查*当前*检查点的IMMEDIATE标志，而是检查
 * 是否有一个在它之后的待处理请求。）
 */
static bool fc_ImmediateCheckpointRequested(void)
{
	volatile CheckpointerShmemStruct *fc_cps = CheckpointerShmem;

	/*
	 * 在这种情况下，我们不需要获取ckpt_lck，因为我们只
	 * 关注单个标志位。
	 */
	if (fc_cps->ckpt_flags & CHECKPOINT_IMMEDIATE)
		return true;
	return false;
}

/*
 * CheckpointWriteDelay -- 控制检查点的速率
 *
 * 此函数在每次BufferSync()执行页面写入后调用。
 * 它负责限制BufferSync()的写入速率，以达到
 * checkpoint_completion_target。
 *
 * 检查点请求标志应该传入；目前唯一检查的是CHECKPOINT_IMMEDIATE，它禁用写入之间的延迟。
 *
 * ‘progress’是已完成工作的估计，作为一个
 * 介于0.0（表示没有）和1.0（表示全部完成）之间的分数。
 */
void CheckpointWriteDelay(int fc_flags, double fc_progress)
{
	static int	fc_absorb_counter = WRITES_PER_ABSORB;

	/* 如果检查点正在非检查点程序中执行，则不执行任何操作 */
	if (!AmCheckpointerProcess())
		return;

	/*
	 * 执行通常的职责并小憩，除非我们落后于计划，
	 * 在这种情况下，我们只是尽快赶上。
	 */
	if (!(fc_flags & CHECKPOINT_IMMEDIATE) &&
		!ShutdownRequestPending &&
		!fc_ImmediateCheckpointRequested() &&
		fc_IsCheckpointOnSchedule(fc_progress))
	{
		if (ConfigReloadPending)
		{
			ConfigReloadPending = false;
			ProcessConfigFile(PGC_SIGHUP);
			/* 更新配置变量的共享内存副本 */
			fc_UpdateSharedMemoryConfig();
		}

		AbsorbSyncRequests();
		fc_absorb_counter = WRITES_PER_ABSORB;

		fc_CheckArchiveTimeout();

		/* 向累计统计系统报告临时统计信息 */
		pgstat_report_checkpointer();

		/*
		 * 这个休眠曾经与 bgwriter_delay 相关，通常是 200 毫秒。
		 * 这导致在没有太多工作要做的时候频繁唤醒。
		 * 检查点和 bgwriter 不再相关，所以采用大休眠。
		 */
		WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH | WL_TIMEOUT,
				  100,
				  WAIT_EVENT_CHECKPOINT_WRITE_DELAY);
		ResetLatch(MyLatch);
	}
	else if (--fc_absorb_counter <= 0)
	{
		/*
		 * 在每 WRITES_PER_ABSORB 次写操作后吸收待处理的 fsync 请求，
		 * 即使我们不休眠，以防止 fsync 请求队列溢出。
		 */
		AbsorbSyncRequests();
		fc_absorb_counter = WRITES_PER_ABSORB;
	}

	/* 检查障碍事件。 */
	if (ProcSignalBarrierPending)
		ProcessProcSignalBarrier();
}

/*
 * IsCheckpointOnSchedule -- 我们是否按计划在时间内完成这个检查点
 *		 (或重启点)？
 *
 * 将当前进度与自上一个检查点以来经过的时间/段进行比较，
 * 如果到目前为止取得的进展大于经过的时间/段，则返回 true。
 */
static bool fc_IsCheckpointOnSchedule(double fc_progress)
{
	XLogRecPtr	fc_recptr;
	struct timeval fc_now;
	double		fc_elapsed_xlogs,
				fc_elapsed_time;

	Assert(ckpt_active);

	/* 根据 checkpoint_completion_target 调整进度。 */
	fc_progress *= CheckPointCompletionTarget;

	/*
	 * 首先检查缓存值。只有在到达之前计算的目标时，
	 * 才进行更昂贵的计算。由于时间或 WAL 插入指针都不会回退，
	 * 新计算的值只能大于或等于缓存值。
	 */
	if (fc_progress < ckpt_cached_elapsed)
		return false;

	/*
	 * 根据写入的 WAL 段和 CheckPointSegments 检查进度。
	 *
	 * 我们将当前 WAL 插入位置与调用 CreateCheckPoint 之前计算的位置进行比较。
	 * 在超过 CheckPointSegments 时实际触发检查点的 XLogInsert 中的代码
	 * 与 RedoRecPtr 进行比较，所以这并不完全准确。
	 * 然而，对于我们的目的来说已经足够好，我们反正只是计算一个估计值。
	 *
	 * 在恢复期间，我们将最后重放的 WAL 记录位置与调用 CreateRestartPoint 之前计算的位置进行比较。
	 * 这保持了正常操作期间检查点的相同步调，但我们可能会超过 max_wal_size 相当多的数量。
	 * 这是因为检查点的重做指针和检查点记录之间可能存在很大差距，只有在看到
	 * 检查点记录后我们才会开始重启点。 (这个差距通常是 CheckPointSegments *
	 * checkpoint_completion_target，其中 checkpoint_completion_target 是生成 WAL 时生效的值)。
	 */
	if (RecoveryInProgress())
		fc_recptr = GetXLogReplayRecPtr(NULL);
	else
		fc_recptr = GetInsertRecPtr();
	fc_elapsed_xlogs = (((double) (fc_recptr - ckpt_start_recptr)) /
					 wal_segment_size) / CheckPointSegments;

	if (fc_progress < fc_elapsed_xlogs)
	{
		ckpt_cached_elapsed = fc_elapsed_xlogs;
		return false;
	}

	/*
	 * 根据经过的时间和 checkpoint_timeout 检查进度。
	 */
	gettimeofday(&fc_now, NULL);
	fc_elapsed_time = ((double) ((pg_time_t) fc_now.tv_sec - ckpt_start_time) +
					fc_now.tv_usec / 1000000.0) / CheckPointTimeout;

	if (fc_progress < fc_elapsed_time)
	{
		ckpt_cached_elapsed = fc_elapsed_time;
		return false;
	}

	/* 看起来我们在计划之内。 */
	return true;
}


/* --------------------------------
 *		信号处理程序例程
 * --------------------------------
 */

/* SIGINT: 设置标志以立即运行正常的检查点 */
static void fc_ReqCheckpointHandler(SIGNAL_ARGS)
{
	int			fc_save_errno = errno;

	/*
	 * 发送信号的进程应该已经将 ckpt_flags 设置为非零，所以我们
	 * 只需要确保我们的主循环能被推出任何等待状态。
	 */
	SetLatch(MyLatch);

	errno = fc_save_errno;
}


/* --------------------------------
 *		与后端的通信
 * --------------------------------
 */

/*
 * CheckpointerShmemSize
 *		计算检查点相关共享内存所需的空间
 */
Size CheckpointerShmemSize(void)
{
	Size		fc_size;

	/*
	 * 目前，requests[] 数组的大小被任意设置为 NBuffers。
	 * 这可能会显得过大或过小 ...
	 */
	fc_size = offsetof(CheckpointerShmemStruct, requests);
	fc_size = add_size(fc_size, mul_size(NBuffers, sizeof(CheckpointerRequest)));

	return fc_size;
}

/*
 * CheckpointerShmemInit
 *		分配并初始化检查点相关的共享内存
 */
void CheckpointerShmemInit(void)
{
	Size		fc_size = CheckpointerShmemSize();
	bool		fc_found;

	CheckpointerShmem = (CheckpointerShmemStruct *)
		ShmemInitStruct("Checkpointer Data",
						fc_size,
						&fc_found);

	if (!fc_found)
	{
		/*
		 * 第一次通过，所以初始化。注意我们将整个
		 * requests 数组置为零；这样 CompactCheckpointerRequestQueue
		 * 可以假定请求结构中的填充字节为零。
		 */
		MemSet(CheckpointerShmem, 0, fc_size);
		SpinLockInit(&CheckpointerShmem->ckpt_lck);
		CheckpointerShmem->max_requests = NBuffers;
		ConditionVariableInit(&CheckpointerShmem->start_cv);
		ConditionVariableInit(&CheckpointerShmem->done_cv);
	}
}

/*
 * RequestCheckpoint
 *		在后端进程中调用以请求一个检查点
 *
 * flags 是以下的按位 OR：
 *	CHECKPOINT_IS_SHUTDOWN: 检查点是为了数据库关闭。
 *	CHECKPOINT_END_OF_RECOVERY: 检查点是为了 WAL 恢复结束。
 *	CHECKPOINT_IMMEDIATE: 尽快完成检查点，
 *		忽略 checkpoint_completion_target 参数。
 *	CHECKPOINT_FORCE: 强制进行检查点，即使自上一个检查点以来没有发生任何 XLOG 活动
 *		（由 CHECKPOINT_IS_SHUTDOWN 或 CHECKPOINT_END_OF_RECOVERY 隐含）。
 *	CHECKPOINT_WAIT: 在返回之前等待完成（否则，
 *		只需通知检查点执行操作，然后返回）。
 *	CHECKPOINT_CAUSE_XLOG: 检查点因 xlog 填满而请求。
 *		（这影响日志记录，特别是启用 CheckPointWarning）。
 */
void RequestCheckpoint(int fc_flags)
{
	int			fc_ntries;
	int			fc_old_failed,
				fc_old_started;

	/*
	 * 如果在独立后端中，只需自己执行。
	 */
	if (!IsPostmasterEnvironment)
	{
		/*
		 * 在独立后端中执行缓慢的检查点毫无意义，
		 * 因为没有其他后端可以中断检查点。
		 */
		CreateCheckPoint(fc_flags | CHECKPOINT_IMMEDIATE);

		/*
		 * 在任何检查点之后，关闭所有 smgr 文件。这样我们就不会
		 * 无限期保留对已删除文件的 smgr 引用。
		 */
		smgrcloseall();

		return;
	}

	
/*
	 * 原子性地设置请求标志，并获取计数器的快照。
	 * 当我们看到 ckpt_started > old_started 时，我们知道这里设置的标志
	 * 已经被检查点程序看到了。
	 *
	 * 注意，我们用任何现有的标志对标志进行或运算，以避免被另一个后端
	 * 覆盖“更强”的请求。标志传感器必须选择得当以使这项工作有效！
	 */
	SpinLockAcquire(&CheckpointerShmem->ckpt_lck);

	fc_old_failed = CheckpointerShmem->ckpt_failed;
	fc_old_started = CheckpointerShmem->ckpt_started;
	CheckpointerShmem->ckpt_flags |= (fc_flags | CHECKPOINT_REQUESTED);

	SpinLockRelease(&CheckpointerShmem->ckpt_lck);

	/*
	 * 发送信号以请求检查点。检查点程序可能尚未启动，
	 * 或正在重新启动过程中，因此如果需要，我们将重试几次。
	 * (实际上，重试次数会多于几次，因为在速度较慢或负载过重的构建
	 * 机器上，观察到检查点程序可能需要几秒钟才能启动。)但是，如果
	 * 没有要求等待检查点发生，我们认为发送信号失败是非致命的，仅
	 * 记录日志。检查点程序在启动时应该看到请求，无论是否接收到
	 * 信号。
	 */
#define MAX_SIGNAL_TRIES 600	/* 最大等待 60.0 秒 */
	for (fc_ntries = 0;; fc_ntries++)
	{
		if (CheckpointerShmem->checkpointer_pid == 0)
		{
			if (fc_ntries >= MAX_SIGNAL_TRIES || !(fc_flags & CHECKPOINT_WAIT))
			{
				elog((fc_flags & CHECKPOINT_WAIT) ? ERROR : LOG,
					 "could not signal for checkpoint: checkpointer is not running");
				break;
			}
		}
		else if (kill(CheckpointerShmem->checkpointer_pid, SIGINT) != 0)
		{
			if (fc_ntries >= MAX_SIGNAL_TRIES || !(fc_flags & CHECKPOINT_WAIT))
			{
				elog((fc_flags & CHECKPOINT_WAIT) ? ERROR : LOG,
					 "could not signal for checkpoint: %m");
				break;
			}
		}
		else
			break;				/* 信号发送成功 */

		CHECK_FOR_INTERRUPTS();
		pg_usleep(100000L);		/* 等待 0.1 秒，然后重试 */
	}

	/*
	 * 如果请求，等待完成。我们根据上面给出的算法检测完成情况。
	 */
	if (fc_flags & CHECKPOINT_WAIT)
	{
		int			fc_new_started,
					fc_new_failed;

		/* 等待新检查点开始。 */
		ConditionVariablePrepareToSleep(&CheckpointerShmem->start_cv);
		for (;;)
		{
			SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
			fc_new_started = CheckpointerShmem->ckpt_started;
			SpinLockRelease(&CheckpointerShmem->ckpt_lck);

			if (fc_new_started != fc_old_started)
				break;

			ConditionVariableSleep(&CheckpointerShmem->start_cv,
								   WAIT_EVENT_CHECKPOINT_START);
		}
		ConditionVariableCancelSleep();

		/*
		 * 我们在模态上等待 ckpt_done >= new_started。
		 */
		ConditionVariablePrepareToSleep(&CheckpointerShmem->done_cv);
		for (;;)
		{
			int			fc_new_done;

			SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
			fc_new_done = CheckpointerShmem->ckpt_done;
			fc_new_failed = CheckpointerShmem->ckpt_failed;
			SpinLockRelease(&CheckpointerShmem->ckpt_lck);

			if (fc_new_done - fc_new_started >= 0)
				break;

			ConditionVariableSleep(&CheckpointerShmem->done_cv,
								   WAIT_EVENT_CHECKPOINT_DONE);
		}
		ConditionVariableCancelSleep();

		if (fc_new_failed != fc_old_failed)
			ereport(ERROR,
					(errmsg("checkpoint request failed"),
					 errhint("Consult recent messages in the server log for details.")));
	}
}

/*
 * ForwardSyncRequest
 *		将文件 fsync 请求从后端转发到检查点程序
 *
 * 每当后端被迫直接写入关系时
 * (如果后台写入程序能完成工作，这种情况应很少发生)，
 * 后端调用此例程，传递关系已被修改且必须在下一个检查点之前进行 fsync
 * 的信息。我们也利用这个机会统计这种写入次数以进行统计用途。
 *
 * 为了避免持有锁的时间过长，我们通常直接写入 requests[] 队列，而不
 * 检查重复项。检查点程序无论如何都需要在内部消除重复项。然而，如果
 * 我们发现队列已满，则会对整个队列进行一次遍历以进行压缩。这会
 * 稍微增加开销，但替代方案是让后端自己执行 fsync，这在实践中要昂贵得多。
 * 理论上，如果队列已满且不包含重复条目，后端 fsync 仍可能是必要的。
 * 在这种情况下，我们通过返回 false 让后端知道。
 */
bool ForwardSyncRequest(const FileTag *fc_ftag, SyncRequestType fc_type)
{
	CheckpointerRequest *fc_request;
	bool		fc_too_full;

	if (!IsUnderPostmaster)
		return false;			/* 可能根本不应该到达这里 */

	if (AmCheckpointerProcess())
		elog(ERROR, "ForwardSyncRequest must not be called in checkpointer");

	LWLockAcquire(CheckpointerCommLock, LW_EXCLUSIVE);

	/* 计算所有后端写入，不管它们是否适合队列 */
	if (!AmBackgroundWriterProcess())
		CheckpointerShmem->num_backend_writes++;

	/* 
	 * 如果检查点进程没有运行或者请求队列已满，后端将必须执行自己的
	 * fsync 请求。但在强制发生之前，我们可以尝试压缩请求队列。
	 */
	if (CheckpointerShmem->checkpointer_pid == 0 ||
		(CheckpointerShmem->num_requests >= CheckpointerShmem->max_requests &&
		 !fc_CompactCheckpointerRequestQueue()))
	{
		/* 
		 * 计算后端必须执行自己 fsync 的写入子集
		 */
		if (!AmBackgroundWriterProcess())
			CheckpointerShmem->num_backend_fsync++;
		LWLockRelease(CheckpointerCommLock);
		return false;
	}

	/* 好的，插入请求 */
	fc_request = &CheckpointerShmem->requests[CheckpointerShmem->num_requests++];
	fc_request->ftag = *fc_ftag;
	fc_request->type = fc_type;

	/* 如果队列超过一半满，则刺激检查点进程将其清空 */
	fc_too_full = (CheckpointerShmem->num_requests >=
				CheckpointerShmem->max_requests / 2);

	LWLockRelease(CheckpointerCommLock);

	/* ... 但在我们释放锁之后 */
	if (fc_too_full && ProcGlobal->checkpointerLatch)
		SetLatch(ProcGlobal->checkpointerLatch);

	return true;
}

/*
 * CompactCheckpointerRequestQueue
 *		删除请求队列中的重复项以避免后端 fsync。
 *		如果移除了任何条目，则返回“true”。
 *
 * 尽管完整的 fsync 请求队列并不常见，但它在发生时可能会导致严重的
 * 性能问题。到目前为止，这种情况仅在系统处于高写入负载时观察到，
 * 特别是在检查点的“同步”阶段。如果没有这个逻辑， 每个后端会对
 * 每个写入的块执行 fsync，这会变得非常昂贵，并可能减慢整个系统。
 *
 * 每次队列满时尝试这样做，如果没有可移除的条目可能会失败。但是在
 * 实践中，这种情况应该极为少见：每个共享缓冲区有一个队列条目。
 */
static bool fc_CompactCheckpointerRequestQueue(void)
{
	struct CheckpointerSlotMapping
	{
		CheckpointerRequest request;
		int			slot;
	};

	int			fc_n,
				fc_preserve_count;
	int			fc_num_skipped = 0;
	HASHCTL		fc_ctl;
	HTAB	   *fc_htab;
	bool	   *fc_skip_slot;

	/* 必须以独占模式持有 CheckpointerCommLock */
	Assert(LWLockHeldByMe(CheckpointerCommLock));

	/* 在关键部分避免内存分配。 */
	if (CritSectionCount > 0)
		return false;

	/* 初始化 skip_slot 数组 */
	fc_skip_slot = palloc0(sizeof(bool) * CheckpointerShmem->num_requests);

	/* 初始化临时哈希表 */
	fc_ctl.keysize = sizeof(CheckpointerRequest);
	fc_ctl.entrysize = sizeof(struct CheckpointerSlotMapping);
	fc_ctl.hcxt = CurrentMemoryContext;

	fc_htab = hash_create("CompactCheckpointerRequestQueue",
					   CheckpointerShmem->num_requests,
					   &fc_ctl,
					   HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);

	/* 
	 * 这里的基本想法是，如果请求后面跟着一个相同的请求，则可以跳过
	 * 该请求。向后检查队列末尾，看请求是否被之前的相同请求“前置”，
	 * 从而希望减少复制，似乎更合理。但如果存在一个干预的 SYNC_FORGET_REQUEST
	 * 或 SYNC_FILTER_REQUEST，这可能会改变语义，因此我们以这种方式
	 * 进行。 如果我们让下面的代码理解此类请求的特定语义，可能会更聪明
	 * （可以删除那些最终会被取消的前面的条目），但不清楚额外的复杂性
	 * 会给我们带来任何好处。
	 */
	for (fc_n = 0; fc_n < CheckpointerShmem->num_requests; fc_n++)
	{
		CheckpointerRequest *fc_request;
		struct CheckpointerSlotMapping *fc_slotmap;
		bool		fc_found;

		/* 
		 * 我们直接使用请求结构作为哈希表键。
		 * 这假设结构中的任何填充字节始终是相同的，这应该没问题
		 * ，因为我们在 CheckpointerShmemInit 中将它们初始化为零。
		 * 注意，RelFileNode最好不要包含任何填充字节。
		 */
		fc_request = &CheckpointerShmem->requests[fc_n];
		fc_slotmap = hash_search(fc_htab, fc_request, HASH_ENTER, &fc_found);
		if (fc_found)
		{
			/* 重复的，因此将前一个发生标记为可跳过 */
			fc_skip_slot[fc_slotmap->slot] = true;
			fc_num_skipped++;
		}
		/* 记住包含该请求值最新出现的槽 */
		fc_slotmap->slot = fc_n;
	}

	/* 处理完哈希表。 */
	hash_destroy(fc_htab);

	/* 如果没有重复项，我们运气不好。 */
	if (!fc_num_skipped)
	{
		pfree(fc_skip_slot);
		return false;
	}

	/* 我们发现了一些重复项；将其删除。 */
	fc_preserve_count = 0;
	for (fc_n = 0; fc_n < CheckpointerShmem->num_requests; fc_n++)
	{
		if (fc_skip_slot[fc_n])
			continue;
		CheckpointerShmem->requests[fc_preserve_count++] = CheckpointerShmem->requests[fc_n];
	}
	ereport(DEBUG1,
			(errmsg_internal("compacted fsync request queue from %d entries to %d entries",
							 CheckpointerShmem->num_requests, fc_preserve_count)));
	CheckpointerShmem->num_requests = fc_preserve_count;

	/* 清理。 */
	pfree(fc_skip_slot);
	return true;
}

/* 
 * AbsorbSyncRequests
 *		检索排队的同步请求并将其传递给同步机制。
 *
 * 由于必须在 CreateCheckPoint 期间调用此函数，因此将其导出；
 * 我们必须确保在开始 fsync 之前已经接受所有待处理的请求。
 * 由于 CreateCheckPoint 有时在非检查点进程中运行，如果不是检查点进程，则不执行任何操作。
 */
void AbsorbSyncRequests(void)
{
	CheckpointerRequest *fc_requests = NULL;
	CheckpointerRequest *fc_request;
	int			fc_n;

	if (!AmCheckpointerProcess())
		return;

	LWLockAcquire(CheckpointerCommLock, LW_EXCLUSIVE);

	/* 将统计计数转移到待处理的 pgstats 消息中 */
	PendingCheckpointerStats.buf_written_backend
		+= CheckpointerShmem->num_backend_writes;
	PendingCheckpointerStats.buf_fsync_backend
		+= CheckpointerShmem->num_backend_fsync;

	CheckpointerShmem->num_backend_writes = 0;
	CheckpointerShmem->num_backend_fsync = 0;

	/* 
	 * 我们尽量避免长时间持有锁，通过复制请求数组，
	 * 在释放锁后处理请求。
	 *
	 * 一旦我们从共享内存中清空了请求，如果随后未能吸收它们（例如，
	 * 因为我们的哈希表耗尽内存），我们必须 PANIC。这是因为如果我们无法
	 * fsync 我们被告知要 fsync 的内容，系统将不能安全运行。
	 * 幸运的是，哈希表非常小，因此在实践中这个问题出现的可能性非常小。
	 */
	fc_n = CheckpointerShmem->num_requests;
	if (fc_n > 0)
	{
		fc_requests = (CheckpointerRequest *) palloc(fc_n * sizeof(CheckpointerRequest));
		memcpy(fc_requests, CheckpointerShmem->requests, fc_n * sizeof(CheckpointerRequest));
	}

	START_CRIT_SECTION();

	CheckpointerShmem->num_requests = 0;

	LWLockRelease(CheckpointerCommLock);

	for (fc_request = fc_requests; fc_n > 0; fc_request++, fc_n--)
		RememberSyncRequest(&fc_request->ftag, fc_request->type);

	END_CRIT_SECTION();

	if (fc_requests)
		pfree(fc_requests);
}

/* 
 * 根据配置参数更新任何共享内存配置 
 */
static void fc_UpdateSharedMemoryConfig(void)
{
	/* 更新用于同步副本的全局 shmem 状态 */
	SyncRepUpdateSyncStandbysDefined();

	/* 
	 * 如果 full_page_writes 被 SIGHUP 更改，我们在共享内存中更新它并写入
	 * XLOG_FPW_CHANGE 记录。
	 */
	UpdateFullPageWrites();

	elog(DEBUG2, "checkpointer updated shared memory configuration values");
}

/* 
 * FirstCallSinceLastCheckpoint 允许进程通过异步检查检查点完成情况，
 * 在每个检查点周期内执行一次操作。
 */
bool FirstCallSinceLastCheckpoint(void)
{
	static int	fc_ckpt_done = 0;
	int			fc_new_done;
	bool		fc_FirstCall = false;

	SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
	fc_new_done = CheckpointerShmem->ckpt_done;
	SpinLockRelease(&CheckpointerShmem->ckpt_lck);

	if (fc_new_done != fc_ckpt_done)
		fc_FirstCall = true;

	fc_ckpt_done = fc_new_done;

	return fc_FirstCall;
}
